﻿/*
 * This file is part of MonoStrategy.
 *
 * Copyright (C) 2010-2011 Christoph Husse
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: 
 *      # Christoph Husse
 * 
 * Also checkout our homepage: http://monostrategy.codeplex.com/
 */
using System;
using System.IO;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.Serialization;

namespace MonoStrategy
{

    public class ResourceStackEntry
    {
        public Point Position { get; set; }
        public Resource Resource { get; set; }
    }
   
    /// <summary>
    /// An animation class represents the topmost animation object. For example it may contains
    /// all animations available for a given object class like a "sawmill". There is an ambient
    /// animation, always visible as animation background. This might be the sawmill itself.
    /// Then there are animation sets. Each set represents another "animation type". Such a type
    /// may consist of several animations, blended together, constructing the final animation
    /// for a given type. There can only be one active type per animation class. Of course you could
    /// just ignore animation types and put the whole thing as single animation with little variations
    /// into the class again and again, but this will just blow up the file size. The animation class
    /// provides a way to split animations into little pieces, saving a lot of file space.
    /// </summary>
    public class AnimationClass
    {
        private UniqueMap<String, AnimationSet> m_Sets = new UniqueMap<string,AnimationSet>();
        private AnimationSet m_AmbientSet;
        
        /// <summary>
        /// The ambient set is always visible in the background (can be null). The animation will be auto-repeated.
        /// </summary>
        public AnimationSet AmbientSet
        {
            get { return m_AmbientSet; }
            set
            {
                ForceWriteable();

                if ((value != null) && !m_Sets.ContainsKey(value.Name))
                    throw new InvalidOperationException("The given ambient set does not belong to this animation class!");

                m_AmbientSet = value;
            }
        }
        public Boolean UseAmbientSet { get { return m_AmbientSet != null; } }
        public List<ResourceStackEntry> ResourceStacks { get; private set; }
        public List<Rectangle> GroundPlane { get; private set; }
        public List<Rectangle> ReservedPlane { get; private set; }
        public Int32 ShiftX { get; set; }
        public Int32 ShiftY { get; set; }

        private void ForceWriteable()
        {
            Library.ForceWriteable();
        }

        internal void Save(BinaryWriter inWriter)
        {
            LoadFrames();

            // swap all bitmap fields to prevent serialization
            DeferredContainer deferred = new DeferredContainer();

            foreach (var set in Sets)
            {
                foreach (var anim in set.Animations)
                {
                    foreach (var frame in anim.Frames)
                    {
                        if (frame.m_Bitmap == null)
                            frame.m_Bitmap = m_Frames[frame.Checksum];

                        deferred.AddFrame(frame);

                        frame.m_Bitmap = null;
                    }
                }
            }

            m_Frames = deferred.Frames;

            // serialize frames
            Stream target = File.OpenWrite(Library.Directory + "/" + Name + ".gfx.tmp");

            using (target)
            {
                deferred.Store(target);
            }

            File.Delete(Library.Directory + "/" + Name + ".gfx");
            File.Move(Library.Directory + "/" + Name + ".gfx.tmp", Library.Directory + "/" + Name + ".gfx");

            // write class object to stream
            inWriter.Write((Byte)2); // class type ID
            inWriter.Write((UInt16)0x1001); // class version

            inWriter.Write((String)Name);
            inWriter.Write((Int32)m_Sets.Count);
            inWriter.Write((Int32)ShiftX);
            inWriter.Write((Int32)ShiftY);

            foreach (var set in m_Sets.Values)
            {
                set.Save(inWriter);
            }

            if (AmbientSet != null)
                inWriter.Write((String)AmbientSet.Name);
            else
                inWriter.Write((String)"");

            inWriter.Write((Int32)ResourceStacks.Count);
            foreach (var stack in ResourceStacks)
            {
                inWriter.Write((sbyte)stack.Position.X);
                inWriter.Write((sbyte)stack.Position.Y);
                inWriter.Write((byte)stack.Resource);
            }

            inWriter.Write((Int32)GroundPlane.Count);
            foreach (var bound in GroundPlane)
            {
                inWriter.Write((sbyte)bound.X);
                inWriter.Write((sbyte)bound.Y);
                inWriter.Write((byte)bound.Width);
                inWriter.Write((byte)bound.Height);
            }

            inWriter.Write((Int32)ReservedPlane.Count);
            foreach (var bound in ReservedPlane)
            {
                inWriter.Write((sbyte)bound.X);
                inWriter.Write((sbyte)bound.Y);
                inWriter.Write((byte)bound.Width);
                inWriter.Write((byte)bound.Height);
            }
        }

        internal static AnimationClass Load(AnimationLibrary inLibrary, BinaryReader inReader)
        {
            AnimationClass result;

            if (inReader.ReadByte() != 2)
                throw new InvalidDataException();

            UInt16 version;
            switch (version = inReader.ReadUInt16())
            {
                case 0x1000:
                case 0x1001:
                    {
                        result = new AnimationClass(inReader.ReadString(), inLibrary);

                        int setCount = inReader.ReadInt32();

                        result.ShiftX = inReader.ReadInt32();
                        result.ShiftY = inReader.ReadInt32();

                        for (int i = 0; i < setCount; i++)
                        {
                            AnimationSet set =  AnimationSet.Load(result, inReader);

                            result.m_Sets.Add(set.Name, set);
                        }

                        String tmp = inReader.ReadString();

                        if (!String.IsNullOrEmpty(tmp))
                            result.AmbientSet = result.m_Sets[tmp];

                        if (version == 0x1001)
                        {
                            for (int i = 0, count = inReader.ReadInt32(); i < count; i++)
                            {
                                sbyte x = inReader.ReadSByte();
                                sbyte y = inReader.ReadSByte();
                                Resource res = (Resource)inReader.ReadByte();

                                result.ResourceStacks.Add(new ResourceStackEntry()
                                {
                                    Position = new Point(x, y),
                                    Resource = res,
                                });
                            }

                            for (int i = 0, count = inReader.ReadInt32(); i < count; i++)
                            {
                                sbyte x = inReader.ReadSByte();
                                sbyte y = inReader.ReadSByte();
                                byte width = inReader.ReadByte();
                                byte height = inReader.ReadByte();

                                result.GroundPlane.Add(new Rectangle(x, y, width, height));
                            }

                            for (int i = 0, count = inReader.ReadInt32(); i < count; i++)
                            {
                                sbyte x = inReader.ReadSByte();
                                sbyte y = inReader.ReadSByte();
                                byte width = inReader.ReadByte();
                                byte height = inReader.ReadByte();

                                result.ReservedPlane.Add(new Rectangle(x, y, width, height));
                            }
                        }
                    } break;

                default:
                    throw new InvalidDataException();
            }

            foreach (var set in result.Sets)
            {
                foreach (var anim in set.Animations)
                {
                    anim.ComputeDimension(false);
                }

                set.ComputeDimension(false);
            }

            result.ComputeDimension();

            return result;
        }

        [OnDeserialized]
        void OnDeserialized(StreamingContext ctx)
        {
            foreach (var set in Sets)
            {
                foreach (var anim in set.Animations)
                {
                    anim.ComputeDimension(false);
                }

                set.ComputeDimension(false);
            }

            ComputeDimension();
        }

        internal void ComputeDimension()
        {
            ForceWriteable();

            Int32 newWidth = 0;
            Int32 newHeight = 0;

            foreach (var set in Sets)
            {
                newWidth = Math.Max(newWidth, set.Width);
                newHeight = Math.Max(newHeight, set.Height);
            }

            Width = newWidth;
            Height = newHeight;

            if (OnChanged != null)
                OnChanged();
        }

        private UniqueMap<Int64, byte[]> m_Frames = null;

        public event DDimensionChangedHandler OnChanged;
        public Int32 Width { get; private set; }
        public Int32 Height { get; private set; }
        public BindingList<AnimationSet> Sets { get { return m_Sets.GetValueBinding(); } }
        public BindingList<AnimationSet> Children { get { return Sets; } }

        public String Name { get; internal set; }
        public AnimationLibrary Library { get; private set; }

        internal AnimationClass(String inName, AnimationLibrary inParent)
        {
            Name = inName;
            Library = inParent;
            ResourceStacks = new List<ResourceStackEntry>();
            GroundPlane = new List<Rectangle>();
            ReservedPlane = new List<Rectangle>();
        }

        public void Rename(AnimationSet inSet, String inNewName)
        {
            ForceWriteable();

            Library.ValidateName(inNewName);

            if (m_Sets.ContainsKey(inNewName))
                throw new ArgumentException("An animation set named \"" + inNewName + "\" does already exist!");

            int pos;

            if ((pos = m_Sets.Values.IndexOf(inSet)) < 0)
                throw new ApplicationException("Animation set does not belong to this set.");

            m_Sets.Remove(inSet.Name);
            inSet.Name = inNewName;
            m_Sets.Add(inSet.Name, inSet);
        }

        public AnimationSet AddAnimationSet(String inName)
        {
            ForceWriteable();

            AnimationSet result = new AnimationSet(inName, this);

            Library.ValidateName(inName);

            if (m_Sets.ContainsKey(inName))
                throw new ArgumentException("An animation set named \"" + inName + "\" does already exist!");

            m_Sets.Add(inName, result);
            ComputeDimension();

            return result;
        }

        public bool ContainsSet(String inName)
        {
            return m_Sets.ContainsKey(inName);
        }

        public bool HasSet(String inName)
        {
            return m_Sets.ContainsKey(inName);
        }

        public AnimationSet FindSet(String inName)
        {
            try
            {
                return m_Sets[inName];
            }
            catch (Exception e)
            {
                throw new ArgumentException("An animation set named \"" + inName + "\" does not exist!", e);
            }
        }

        public void RemoveAnimationSet(AnimationSet inAnimationSet)
        {
            ForceWriteable();

            m_Sets.Remove(inAnimationSet.Name);

            if ((AmbientSet != null) && (AmbientSet.Name.CompareTo(inAnimationSet.Name) == 0))
            {
                AmbientSet = null;
            }

            ComputeDimension();
        }

        internal void LoadFrames()
        {
            if (m_Frames == null)
            {
                if (!File.Exists(Library.Directory + "/" + Name + ".gfx"))
                {
                    m_Frames = new UniqueMap<long, byte[]>();
                }
                else
                {
                    // deferred load
                    Stream source = File.OpenRead(Library.Directory + "/" + Name + ".gfx");

                    using (source)
                    {
                        DeferredContainer deferred = DeferredContainer.Load(source);

                        m_Frames = deferred.Frames;
                    }
                }
            }
        }

        internal byte[] LoadFrame(AnimationFrame inFrame)
        {
            byte[] result;

            LoadFrames();

            if (!m_Frames.TryGetValue(inFrame.Checksum, out result))
                throw new FileNotFoundException("Failed to load shared frame bitmap (Checksum: " + inFrame.Checksum + ") in animation \"" + inFrame.AnimationOrNull.Path + "\".");

            return result;
        }
    }
}
